﻿/*
   Copyright 2012 Dmitry Bratus

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Flower.Services.Data;
using Jurassic.Library;
using Flower.Directory.Util;
using Jurassic;
using System.Xml;
using System.IO;
using System.IO.Compression;

namespace Flower.Directory.Scripting
{
    public class XmlObject : ObjectInstance
    {
        private Blob _blob;
        private Lazy<XmlNode> _xml;
        private readonly BlobFormat _format;

        public XmlNode Xml { get { return _xml.Value; } }

        public XmlObject(ObjectInstance prototype, string xml, BlobFormat format)
            : base(prototype)
        {
            PopulateFunctions();

            _format = format;
            _xml = new Lazy<XmlNode>
            (
                () =>
                {
                    var xmlDoc = new XmlDocument();
                    xmlDoc.LoadXml(xml);
                    return xmlDoc;
                }
            );
        }

        public XmlObject(ObjectInstance prototype, Blob blob)
            : base(prototype)
        {
            PopulateFunctions();

            _format = blob.Format;
            _blob = blob;
            _xml = new Lazy<XmlNode>
            (
                () =>
                {
                    var xmlDoc = new XmlDocument();
                    BlobUtil.ReadBlob(_blob, xmlDoc.Load);
                    _blob = null;
                    return xmlDoc;
                }
            );
        }

        public XmlObject(ObjectInstance prototype, XmlNode xml, BlobFormat format)
            : base(prototype)
        {
            PopulateFunctions();

            _format = format;
            _xml = new Lazy<XmlNode>(() => xml);
        }

        public Blob ToBlob()
        {
            return (_blob != null) ? _blob : BlobUtil.WriteBlob(_format, writer => Xml.WriteTo(writer));
        }

        private XmlDocument XmlDocument
        {
            get
            {
                var xmlDoc = Xml as XmlDocument;

                if (xmlDoc == null)
                {
                    if (Xml.OwnerDocument == null)
                    {
                        return null;
                    }
                    else
                    {
                        xmlDoc = Xml.OwnerDocument;
                    }
                }

                return xmlDoc;
            }
        }

        [JSFunction(Name = "xpath")]
        public ArrayInstance XPath(string xpath)
        {
            var array = Engine.Array.Construct();

            var xmlDoc = XmlDocument;
            if (xmlDoc == null)
            {
                return array;
            }

            var nsManager = new XmlNamespaceManager(xmlDoc.NameTable);

            var xmlns = Engine.GetGlobalValue("$xmlns") as ObjectInstance;
            if (xmlns != null)
            {
                foreach (var prop in xmlns.Properties)
                {
                    if (prop.Value != null && !(prop.Value is Null) && !(prop.Value is Undefined))
                    {
                        nsManager.AddNamespace(prop.Name, prop.Value.ToString());
                    }
                }
            }

            var xmlElements = Xml.SelectNodes(xpath, nsManager);
            if (xmlElements != null)
            {
                foreach (XmlNode xmlElement in xmlElements)
                {
                    array.Push((object)new XmlObject(Engine.Object.InstancePrototype, xmlElement, _format));
                }
            }

            return array;
        }

        [JSFunction(Name = "children")]
        public ArrayInstance Children()
        {
            var array = Engine.Array.Construct();

            foreach (XmlNode child in Xml.ChildNodes)
            {
                array.Push((object)new XmlObject(Engine.Object.InstancePrototype, child, _format));
            }

            return array;
        }

        [JSFunction(Name = "childrenCount")]
        public int ChildrenCount()
        {
            return Xml.ChildNodes.Count;
        }

        [JSFunction(Name = "attr")]
        public object Attr(string name, object value)
        {
            if (Xml.Attributes != null)
            {
                XmlAttribute attr = Xml.Attributes[name];

                if (value != null && !(value is Undefined) && !(value is Null))
                {
                    if (attr == null)
                    {
                        var xmlDoc = XmlDocument;
                        if (xmlDoc == null)
                        {
                            return this;
                        }

                        attr = xmlDoc.CreateAttribute(name);
                        attr.Value = value.ToString();
                        Xml.Attributes.Append(attr);
                    }
                    else
                    {
                        attr.Value = value.ToString();
                    }

                    return this;
                }
                else
                {
                    if (attr != null)
                    {
                        return attr.Value;
                    }
                }
            }

            return Undefined.Value;
        }

        [JSFunction(Name = "innerText")]
        public object InnerText(object text)
        {
            if (text != null && !(text is Undefined))
            {
                Xml.InnerText = text.ToString();
                return this;
            }
            else
            {
                return Xml.InnerText;
            }
        }

        [JSFunction(Name = "append")]
        public XmlObject Append(object node)
        {
            var xmlNode = node as XmlObject;
            
            if (xmlNode == null)
            {
                return this;
            }

            var xmlDoc = XmlDocument;
            if (xmlDoc == null)
            {
                return this;
            }

            XmlNode nodeToAdd =
                (xmlNode.Xml is XmlDocument) ? ((XmlDocument)xmlNode.Xml).DocumentElement : xmlNode.Xml;

            if (nodeToAdd != null)
            {
                Xml.AppendChild(xmlDoc.ImportNode(nodeToAdd, true));    
            }
            
            return this;
        }

        [JSFunction(Name = "prepend")]
        public XmlObject Prepend(object node)
        {
            var xmlNode = node as XmlObject;

            if (xmlNode == null)
            {
                return this;
            }

            var xmlDoc = XmlDocument;
            if (xmlDoc == null)
            {
                return this;
            }

            XmlNode nodeToAdd =
                (xmlNode.Xml is XmlDocument) ? ((XmlDocument)xmlNode.Xml).DocumentElement : xmlNode.Xml;

            if (nodeToAdd != null)
            {
                Xml.PrependChild(xmlDoc.ImportNode(nodeToAdd, true));
            }

            return this;
        }

        [JSFunction(Name = "insertBefore")]
        public XmlObject InsertBefore(object node, object refNode)
        {
            var xmlNode = node as XmlObject;
            var xmlRefNode = refNode as XmlObject;

            if (xmlNode == null || xmlRefNode == null)
            {
                return this;
            }

            var xmlDoc = XmlDocument;
            if (xmlDoc == null)
            {
                return this;
            }

            XmlNode nodeToAdd =
                (xmlNode.Xml is XmlDocument) ? ((XmlDocument)xmlNode.Xml).DocumentElement : xmlNode.Xml;

            if (nodeToAdd != null)
            {
                Xml.InsertBefore(xmlDoc.ImportNode(nodeToAdd, true), xmlRefNode.Xml);
            }

            return this;
        }

        [JSFunction(Name = "insertAfter")]
        public XmlObject InsertAfter(object node, object refNode)
        {
            var xmlNode = node as XmlObject;
            var xmlRefNode = refNode as XmlObject;

            if (xmlNode == null || xmlRefNode == null)
            {
                return this;
            }

            var xmlDoc = XmlDocument;
            if (xmlDoc == null)
            {
                return this;
            }

            XmlNode nodeToAdd =
                (xmlNode.Xml is XmlDocument) ? ((XmlDocument)xmlNode.Xml).DocumentElement : xmlNode.Xml;

            if (nodeToAdd != null)
            {
                Xml.InsertAfter(xmlDoc.ImportNode(nodeToAdd, true), xmlRefNode.Xml);
            }

            return this;
        }

        [JSFunction(Name = "remove")]
        public XmlObject Remove()
        {
            if (Xml.ParentNode == null) throw new InvalidOperationException("The node is root.");

            Xml.ParentNode.RemoveChild(Xml);

            return this;
        }

        [JSFunction(Name = "asBool")]
        public object AsBool(object value)
        {
            if (value == null || value is Null || value is Undefined)
            {
                return ToBlob().To<bool>();
            }
            else if (value is bool)
            {
                _blob = null;
                _xml = new Lazy<XmlNode>
                (
                    () =>
                    {
                        var xmlDoc = new XmlDocument();
                        BlobUtil.ReadBlob(((bool)value).ToBlob(BlobFormat.BinXml), xmlDoc.Load);
                        return xmlDoc;
                    }
                );
            }

            return this;
        }

        [JSFunction(Name = "asInt")]
        public object AsInt(object value)
        {
            if (value == null || value is Null || value is Undefined)
            {
                return ToBlob().To<int>();
            }
            else if (value is int)
            {
                _blob = null;
                _xml = new Lazy<XmlNode>
                (
                    () =>
                    {
                        var xmlDoc = new XmlDocument();
                        BlobUtil.ReadBlob(((int)value).ToBlob(BlobFormat.BinXml), xmlDoc.Load);
                        return xmlDoc;
                    }
                );
            }
            else if (value is uint)
            {
                _blob = null;
                _xml = new Lazy<XmlNode>
                (
                    () =>
                    {
                        var xmlDoc = new XmlDocument();
                        BlobUtil.ReadBlob(((int)(uint)value).ToBlob(BlobFormat.BinXml), xmlDoc.Load);
                        return xmlDoc;
                    }
                );
            }
            else if (value is double)
            {
                _blob = null;
                _xml = new Lazy<XmlNode>
                (
                    () =>
                    {
                        var xmlDoc = new XmlDocument();
                        BlobUtil.ReadBlob(((int)(double)value).ToBlob(BlobFormat.BinXml), xmlDoc.Load);
                        return xmlDoc;
                    }
                );
            }

            return this;
        }

        [JSFunction(Name = "asDouble")]
        public object AsDouble(object value)
        {
            if (value == null || value is Null || value is Undefined)
            {
                return ToBlob().To<double>();
            }
            else if (value is int)
            {
                _blob = null;
                _xml = new Lazy<XmlNode>
                (
                    () =>
                    {
                        var xmlDoc = new XmlDocument();
                        BlobUtil.ReadBlob(((double)(int)value).ToBlob(BlobFormat.BinXml), xmlDoc.Load);
                        return xmlDoc;
                    }
                );
            }
            else if (value is uint)
            {
                _blob = null;
                _xml = new Lazy<XmlNode>
                (
                    () =>
                    {
                        var xmlDoc = new XmlDocument();
                        BlobUtil.ReadBlob(((double)(uint)value).ToBlob(BlobFormat.BinXml), xmlDoc.Load);
                        return xmlDoc;
                    }
                );
            }
            else if (value is double)
            {
                _blob = null;
                _xml = new Lazy<XmlNode>
                (
                    () =>
                    {
                        var xmlDoc = new XmlDocument();
                        BlobUtil.ReadBlob(((double)value).ToBlob(BlobFormat.BinXml), xmlDoc.Load);
                        return xmlDoc;
                    }
                );
            }

            return this;
        }

        [JSFunction(Name = "asString")]
        public object AsString(object value)
        {
            if (value == null || value is Null || value is Undefined)
            {
                return ToBlob().To<string>();
            }
            else if (value is string)
            {
                _blob = null;
                _xml = new Lazy<XmlNode>
                (
                    () =>
                    {
                        var xmlDoc = new XmlDocument();
                        BlobUtil.ReadBlob(((string)value).ToBlob(BlobFormat.BinXml), xmlDoc.Load);
                        return xmlDoc;
                    }
                );
            }

            return this;
        }
    }

    public class XmlObjectConstructor : ClrFunction
    {
        public XmlObjectConstructor(ScriptEngine engine)
            : base(
                engine.Function.InstancePrototype,
                "xml",
                engine.Object.InstancePrototype
            )
        {
        }

        [JSCallFunction]
        public XmlObject Create(string xml, int format)
        {
            return new XmlObject(InstancePrototype, xml, (BlobFormat)format);
        }
    }

    public class BoolToXmlFunction : ClrFunction
    {
        public BoolToXmlFunction(ScriptEngine engine)
            : base(
                engine.Function.InstancePrototype,
                "boolToXml",
                engine.Object.InstancePrototype
            )
        {
        }

        [JSCallFunction]
        public XmlObject Create(bool value, int format)
        {
            return new XmlObject(InstancePrototype, value.ToBlob((BlobFormat)format));
        }
    }

    public class IntToXmlFunction : ClrFunction
    {
        public IntToXmlFunction(ScriptEngine engine)
            : base(
                engine.Function.InstancePrototype,
                "intToXml",
                engine.Object.InstancePrototype
            )
        {
        }

        [JSCallFunction]
        public XmlObject Create(int value, int format)
        {
            return new XmlObject(InstancePrototype, value.ToBlob((BlobFormat)format));
        }
    }

    public class DoubleToXmlFunction : ClrFunction
    {
        public DoubleToXmlFunction(ScriptEngine engine)
            : base(
                engine.Function.InstancePrototype,
                "doubleToXml",
                engine.Object.InstancePrototype
            )
        {
        }

        [JSCallFunction]
        public XmlObject Create(double value, int format)
        {
            return new XmlObject(InstancePrototype, value.ToBlob((BlobFormat)format));
        }
    }

    public class StringToXmlFunction : ClrFunction
    {
        public StringToXmlFunction(ScriptEngine engine)
            : base(
                engine.Function.InstancePrototype,
                "stringToXml",
                engine.Object.InstancePrototype
            )
        {
        }

        [JSCallFunction]
        public XmlObject Create(string value, int format)
        {
            return new XmlObject(InstancePrototype, value.ToBlob((BlobFormat)format));
        }
    }
}
